﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.ComponentModel;
using Asol.Reporting.Editor.Editor;
using Asol.Reporting.Editor.Data;
using Asol.Reporting.Editor.Components;
using Asol.Reporting.Editor.ComponentsLayer;
using System.Drawing.Drawing2D;
using Asol.Reporting.Support;
using Asol.Reporting.Support.Components;

namespace Asol.Reporting.Editor.ReportObjects
{
    #region class ReportGraphPie : komponenta reportu, která zobrazuje graf typu Pie (koláč)
    /// <summary>
    /// Třída zobrazující graf 3D Pie (koláč)
    /// </summary>
    public class ReportGraphPie : ReportGraph3D
    {
        #region Konstrukce
        public ReportGraphPie()
        {
            this.EditorData = new GraphDataPie(this);
            this.EditorData.FillValuesForEditor();
            this.Tilt = 0.333f;
        }
        #endregion
        #region Vlastnosti v Menu + Toolbaru (UI)
        /// <summary>
        /// Property, která obsahuje veškeré informace pro tvorbu UI
        /// </summary>
        protected override MenuItemInfo MenuInfo
        {
            get
            {
                return new MenuItemInfo()
                {
                    Availability = MenuItemAvailability.Users,
                    GroupName = "20",
                    OrderInGroup = 15,
                    Name = "Graf Pie3D",
                    TooltipText = "Vloží graf typu Pie3D (koláč)",
                    ImageSmall = Asol.Reporting.Editor.Pics.IconLibrary22.InsertChartPie_FromFile,
                    ImageMedium = null
                };
            }
        }
        #endregion
        #region Overrides pro editor: příprava 3D segmentů, obsluha UserDefined 3D segmentů (interaktivita, vykreslování)
        /// <summary>
        /// Stupeň náklonu: 0 = kolmo k uživateli, +1 = leží otočeno dolů, -1 = leží otočno nahoru
        /// </summary>
        [Browsable(false)]
        public float Tilt
        {
            get { return _Tilt; }
            set
            {
                if (value < -0.95f) _Tilt = -0.95f;
                else if (value > 0.95f) _Tilt = 0.95f;
                else _Tilt = value;
            }
        }
        private float _Tilt;
        /// <summary>
        /// Příprava interaktivních segmentů v režimu Select
        /// </summary>
        /// <param name="args"></param>
        protected override void PrepareInteractiveSegmentsSelect(ReportObjectPrepareSegmentsArgs args)
        {
            base.PrepareInteractiveSegmentsSelect(args);

            if (args.IsWithMouse)
            {
                float tilt = this.Tilt;
                float y = args.CurrentVisualBound.Y + ((tilt + 1f) / 2f) * args.CurrentVisualBound.Height;
                float x = this.GetTiltX(this.GetTiltArea(args.CurrentVisualBound), y);
                PointF tiltPoint = new PointF(x, y);
                args.PrepareInteractiveSegmentsOne(this, UserSegmentType.TiltPoint, true,
                    new SheetF(tiltPoint.CreateRectangleFromCenter(8f)),
                    tiltPoint,
                    tiltPoint.CreateRectangleFromCenter(6f),
                    SysCursorType.SizeAll, SysCursorType.SizeAll, SysCursorType.NoMoveAuto,
                    EditorItemSegmentGripType.UserDefined,
                    true, true, false, true, false,
                    EditMouseChangeType.OnChange);
            }
        }
        /// <summary>
        /// Vykreslí UserDefined segmenty
        /// </summary>
        /// <param name="args"></param>
        /// <param name="segment"></param>
        protected override void EditorDrawSegmentUser(ReportObjectDrawArgs args, EditorItemSegment segment)
        {
            base.EditorDrawSegmentUser(args, segment);
            if (segment != null && segment.SegmentType is UserSegmentType)
            {
                UserSegmentType type = (UserSegmentType)segment.SegmentType;
                switch (type)
                {
                    case UserSegmentType.TiltPoint:
                        this.EditorDrawSegmentUserTiltPoint(args, segment);
                        break;
                }
            }
        }
        /// <summary>
        /// Vykreslí segment typu UserSegmentType.TargetPoint
        /// </summary>
        /// <param name="args"></param>
        /// <param name="segment"></param>
        protected virtual void EditorDrawSegmentUserTiltPoint(ReportObjectDrawArgs args, EditorItemSegment segment)
        {
            using (Brush brush = this.EditorGetBrushForSegment(args, segment))
            using (Pen pen = this.EditorGetPenForTilt(args, segment))
            {
                var state = args.Graphics.Save();
                args.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

                PointF tiltPoint = segment.SegmentPoint;

                // Tilt se kreslí jako bod na pravé polovině elipsy:
                GraphicsPath ellipsePath = GetTiltPath(args.CurrentVisualBound);
                args.Graphics.DrawPath(pen, ellipsePath);
                args.Graphics.FillEllipse(brush, segment.SegmentVisualBound);

                args.Graphics.Restore(state);
            }
        }
        /// <summary>
        /// Vrátí pero pro kreslení elipsy pro náklon (Tilt)
        /// </summary>
        /// <param name="args"></param>
        /// <param name="segment"></param>
        /// <returns></returns>
        protected virtual Pen EditorGetPenForTilt(ReportObjectDrawArgs args, EditorItemSegment segment)
        {
            Pen pen = new Pen(Color.Pink, 1f);
            pen.DashStyle = DashStyle.Dot;
            return pen;
        }
        /// <summary>
        /// Přetahování UserDefined segmentů
        /// </summary>
        /// <param name="args"></param>
        protected override void EditorDragUserSegment(ReportObjectInteractiveSegmentsArgs args)
        {
            base.EditorDragUserSegment(args);
            if (!args.Handled)
            {   // Pokud to nevyřešil předek, možná to vyřešíme my:
                UserSegmentType type = (UserSegmentType)args.ActiveSegment.SegmentType;
                switch (type)
                {
                    case UserSegmentType.TiltPoint:
                        this.EditorDragUserSegmentTiltPoint(args);
                        break;
                }
            }
        }
        /// <summary>
        /// Přetahování UserDefined segmentu typu TiltPoint
        /// </summary>
        /// <param name="args"></param>
        private void EditorDragUserSegmentTiltPoint(ReportObjectInteractiveSegmentsArgs args)
        {
            // Segment se přetahuje nahoru / dolů, od souřadnice Y se určí Tilt v rozmezí -1 až +1:
            float y = args.CurrentVisualBound.GetPointFRelative(args.MousePointF).Y;     // Pozice na ose Y v rozsahu 0 (nahoře) až 1 (dole)
            this.Tilt = 2f * y - 1f;           // Změnit pozici na -1 až +1, přičemž set {} omezí hodnoty do rozsahu -0.95 až +0.95

            args.Handled = true;
            args.ReDrawInteractive = true;
        }
        #region Podpora pro výpočty elipsy a bodu na ní
        /// <summary>
        /// Vrátí GraphicsPath, která vykreslí dráhu bodu TiltPoint
        /// </summary>
        /// <param name="bounds"></param>
        /// <returns></returns>
        private GraphicsPath GetTiltPath(RectangleF bounds)
        {
            GraphicsPath path = new GraphicsPath();
            RectangleF tiltArea = GetTiltArea(bounds);
            float angle1 = (float)GetTiltAngle(tiltArea, bounds.Top);
            float angle2 = (float)GetTiltAngle(tiltArea, bounds.Bottom);
            path.AddArc(tiltArea, angle1, angle2 - angle1);
            return path;
        }
        /// <summary>
        /// Vrátí souřadnice elipsy ve tvaru RectangleF (udává její prostor)
        /// </summary>
        /// <param name="bounds"></param>
        /// <returns></returns>
        private RectangleF GetTiltArea(RectangleF bounds)
        {
            float h = 1.06f * bounds.Height;
            float w = 0.25f * h;
            if (w > bounds.Width) w = bounds.Width;
            return bounds.Center().CreateRectangleFromCenter(w, h);
        }
        /// <summary>
        /// Vrátí úhel, na kterém v dané elipse leží bod, jehož souřadnice Y je daná
        /// </summary>
        /// <param name="tiltArea"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private float GetTiltAngle(RectangleF tiltArea, float y)
        {
            // Vyřeším mezní situace:
            if (tiltArea.Width <= 0f) return 0f;
            if (tiltArea.Height <= 0f) return 0f;

            // Pracujeme s elipsou, takže nemám přímé nástroje na přepočty souřadnic a úhlů.
            // Pomůžu si kružnicí ve stejném středu a s poloměrem r = poloměr elipsy ve směru y (protože mám zadanou souřadnici Y):
            PointF center = tiltArea.Center();
            if (y == center.Y) return 0f;                              // Úspora výpočtu: pokud je zadaná souřadnice rovna středu, vracím 0°
            if (y >= tiltArea.Bottom) return 90f;
            if (y <= tiltArea.Top) return -90f;

            // Vypočtu souřadnici X odpovídající k zadané souřadnici Y (goniometrie), ale na ideální kružnici namísto na elipse:
            float radiusY = tiltArea.Height / 2f;
            double dy = ((double)(y - center.Y)) / radiusY;            // Vzdálenost zadaného bodu Y od středu kružnice, v hodnotách -1 až 1
            double angleC = Math.Acos(dy);
            if (tiltArea.Width == tiltArea.Height) return GetAngleDegress(angleC);   // Pokud je náhodou zadána kružnice, pak výsledek je právě zjištěný úhel.
            // Najdu souřadnici bodu X na elipse (elipsa je de facto proporcionálně změněná kružnice):
            double dxc = Math.Sin(angleC);                                           // Vzdálenost bodu na ideální kružnici, na ose X, se shodným úhlem
            double ratio = (double)tiltArea.Width / (double)tiltArea.Height;         // Poměr šířky elipsy k její výšce
            double dxe = dxc * ratio;
            double angleE = Math.Atan(dy / dxe);
            return GetAngleDegress(angleE);
        }
        /// <summary>
        /// Vrátí souřadnici X, na které v dané elipse leží bod, jehož souřadnice Y je daná
        /// </summary>
        /// <param name="tiltArea"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private float GetTiltX(RectangleF tiltArea, float y)
        {
            // Vyřeším mezní situace:
            if (tiltArea.Width <= 0f) return tiltArea.Right;
            if (tiltArea.Height <= 0f) return tiltArea.Right;

            // Pracujeme s elipsou, takže nemám přímé nástroje na přepočty souřadnic a úhlů.
            // Pomůžu si kružnicí ve stejném středu a s poloměrem r = poloměr elipsy ve směru y (protože mám zadanou souřadnici Y):
            PointF center = tiltArea.Center();
            if (y == center.Y) return tiltArea.Right;                  // Úspora výpočtu: pokud je zadaná souřadnice rovna středu, vracím Right
            if (y >= tiltArea.Bottom) return center.X;
            if (y <= tiltArea.Top) return center.X;

            // Vypočtu souřadnici X odpovídající k zadané souřadnici Y (goniometrie), ale na ideální kružnici namísto na elipse:
            float radiusY = tiltArea.Height / 2f;
            double dy = ((double)(y - center.Y)) / radiusY;            // Vzdálenost zadaného bodu Y od středu kružnice, v hodnotách -1 až 1
            double angleC = Math.Acos(dy);
            double dx = Math.Sin(angleC);                              // Vzdálenost bodu na ideální kružnici, na ose X, se shodným úhlem
            return (float)dx * tiltArea.Width / 2f + center.X;         // Vzdálenost bodu na elipse se určí velikostí poloosy elipsy ve směru X = (Width / 2).
        }
        private float GetAngleDegress(double angle)
        {
            return (float)(angle * 360d / (2d * Math.PI));
        }
        #endregion
        #endregion
        #region Vykreslení v editoru, SizeChange
        /// <summary>
        /// Data grafu pro tisk reportu
        /// </summary>
        [Browsable(false)]
        public GraphDataPie EditorData { get; private set; }
        /// <summary>
        /// Vytvoří a vrátí sadu střípků, které reprezentují graf. Pro editaci (na základě dat v this.EditorData).
        /// </summary>
        /// <param name="currentVisualBound"></param>
        /// <returns></returns>
        protected override IEnumerable<GraphDrawPiece> EditorGetGraphPieces(RectangleF currentVisualBound)
        {
            return this.EditorData.GetGraphPieces(currentVisualBound);
        }
        /// <summary>
        /// Voláno v době editace.
        /// Umožní prvku, aby korigoval souřadnice a/nebo velikost prvku.
        /// </summary>
        /// <param name="args"></param>
        protected override void EditorLogicalBoundsChange(ReportObjectLogicalBoundsChangeArgs args)
        {
            switch (args.ChangeType)
            {
                case LogicalBoundsChangeType.Insert:
                    // Vkládání nového objektu: bod počátku lze ignorovat, zde nastavíme optimální úvodní logickou velikost (v milimetrech):
                    args.LogicalBounds = new RectangleF(0, 0, 60f, 40f);
                    break;
            }
        }
        #endregion
    }
    #endregion
    #region class GraphDataPie : datová třída, jejímž účelem je vypočítat data grafu Bar a vypočtená data převést na grafické prvky metodou GetGraphPieces()
    /// <summary>
    /// GraphDataPie : datová třída, jejímž účelem je vypočítat data grafu Bar a vypočtená data převést na grafické prvky metodou GetGraphPieces()
    /// </summary>
    public class GraphDataPie : GraphData<GraphDataItemPieInfo>
    {
        #region Konstrukce
        public GraphDataPie(ReportGraphPie owner)
        {
            this.Owner = owner;
        }
        /// <summary>
        /// Vlastník grafu, obsahuje parametry pro zobrazení grafu
        /// </summary>
        protected ReportGraphPie Owner { get; private set; }
        /// <summary>
        /// Aktuální dimenze grafu
        /// </summary>
        protected Block3D CurrentBlock { get; private set; }
        #endregion
        #region Tvorba dat pro zobrazení v editoru
        /// <summary>
        /// Naplní do this data pro zobrazení v editoru
        /// </summary>
        public override void FillValuesForEditor()
        {
            this.GraphItemList = new List<GraphDataItem<GraphDataItemPieInfo>>();
            this.GraphItemList.Add(new GraphDataItem<GraphDataItemPieInfo>() { Position = 4, Series = 0, Value = 100m, Caption = "Ddd", GraphColor = Color.Wheat });
            this.GraphItemList.Add(new GraphDataItem<GraphDataItemPieInfo>() { Position = 1, Series = 0, Value = 250m, Caption = "Aaa", GraphColor = Color.LightCyan });
            this.GraphItemList.Add(new GraphDataItem<GraphDataItemPieInfo>() { Position = 2, Series = 0, Value = 700m, Caption = "Bbb", GraphColor = Color.Linen });
            this.GraphItemList.Add(new GraphDataItem<GraphDataItemPieInfo>() { Position = 3, Series = 0, Value = 1400m, Caption = "Ccc", GraphColor = Color.MistyRose });
            this.GraphItemList.Add(new GraphDataItem<GraphDataItemPieInfo>() { Position = 5, Series = 0, Value = 1800m, Caption = "Eee", GraphColor = Color.Gainsboro });
        }
        #endregion
        #region Generování grafických prvků
        /// <summary>
        /// Klíčová metoda každého grafu, kdy transformuje svoje logická data do vizuálních střípků, které tvoří aktuální reprezentaci grafu.
        /// Zobrazování provádí 
        /// </summary>
        public override IEnumerable<GraphDrawPiece> GetGraphPieces(RectangleF bounds)
        {
            List<GraphDrawPiece> result = new List<GraphDrawPiece>();
            View3D view3D = this.Owner.CreateView3D(bounds);
            this.AddGraphPiecesBound(bounds, view3D, result);
            this.AddGraphPiecesDataItems(bounds, view3D, result);
            return result;
        }
        protected void AddGraphPiecesBound(RectangleF bounds, View3D view3D, List<GraphDrawPiece> result)
        {
            this.CurrentBlock = this.Owner.CreateGraphBlock3D(bounds);
            this.AddGraphPiecesBack(bounds, this.CurrentBlock, view3D, this.Owner.BackColor, result);
        }
        protected void AddGraphPiecesDataItems(RectangleF bounds, View3D view3D, List<GraphDrawPiece> result)
        {
            List<GraphDataItem<GraphDataItemPieInfo>> items = this.PrepareDataItems(view3D);

            foreach (GraphDataItem<GraphDataItemPieInfo> item in items)
                this.AddGraphPiecesDataItemOne(bounds, view3D, item, result);
        }
        protected void AddGraphPiecesDataItemOne(RectangleF bounds, View3D view3D, GraphDataItem<GraphDataItemPieInfo> item, List<GraphDrawPiece> result)
        {
            System.Drawing.Drawing2D.GraphicsPath path = new GraphicsPath();
            path.AddPie(Rectangle.Round(item.GraphItemInfo.PieArea), item.GraphItemInfo.StartAngle, item.GraphItemInfo.SweepAngle);
            GraphDrawPiece piece = new GraphDrawPiece()
            {
                PieceType = GraphDrawPieceType.Path,
                Bounds = bounds,
                FillColor = item.GraphColor,
                LineColor = Color.DarkGray,
                LineWidth = 1f,
                Path = path
            };
            result.Add(piece);
        }
        #endregion
        #region Příprava prostorů grafu = rozmisťování objektů a určení jejich velikosti podle typu grafu a číselných hodnot prvků
        /// <summary>
        /// Připraví data pro kreslení grafu: převezme seznam this.GraphItemList, a do jeho položek do property GraphItemInfo vloží aktuální vizuální data
        /// </summary>
        /// <param name="view3D"></param>
        /// <returns></returns>
        private List<GraphDataItem<GraphDataItemPieInfo>> PrepareDataItems(View3D view3D)
        {
            List<GraphDataItem<GraphDataItemPieInfo>> items = new List<GraphDataItem<GraphDataItemPieInfo>>();

            List<GraphDataItem<GraphDataItemPieInfo>> list = this.GetSortedItems(this.Owner.SortType);
            if (list == null) return items;

            int itemCount = list.Count;

            Point3D center3D = this.CurrentBlock.Center;
            Size3D size3D = this.CurrentBlock.Size;
            PointF center = new PointF(center3D.X, center3D.Y);

            float pieUsage = 0.92f;
            double pieTiltAngle = Math.PI / 2d * (double)this.Owner.Tilt;
            float pieTiltCos = (float)Math.Cos(pieTiltAngle);
            float pieWidth = pieUsage * size3D.Width;
            float pieHeight = pieUsage * size3D.Height * pieTiltCos;
            if (pieHeight <= 5f)
                pieHeight = 5f;
            RectangleF pieArea = center.CreateRectangleFromCenter(new SizeF(pieWidth, pieHeight));

            float radius = pieWidth / 2f;

            decimal valueSum = this.ValueSum.Value;
            decimal valueAngle = 360m / valueSum;
            float angle = -90f;
            foreach (GraphDataItem<GraphDataItemPieInfo> item in list)
            {
                item.GraphItemInfo = new GraphDataItemPieInfo();
                item.GraphItemInfo.ArcCenter = center;
                item.GraphItemInfo.PieArea = pieArea;
                item.GraphItemInfo.TiltAngle = pieTiltAngle;
                item.GraphItemInfo.Radius = radius;
                item.GraphItemInfo.StartAngle = angle;
                item.GraphItemInfo.SweepAngle = (float)(valueAngle * item.Value);
                item.GraphItemInfo.ObserverDistance = 0f;
                items.Add(item);
                angle += item.GraphItemInfo.SweepAngle;
            }

            items.Sort(GraphDataItemPieInfo.CompareByObserverDistanceDesc);

            return items;
        }
        private Block3D CreateItemBlock(float x, float b, float w, float d, float hMax, decimal valueMax, decimal value)
        {
            float h = ((valueMax == 0m || value == 0m) ? 0f : hMax * (float)(value / valueMax));
            return new Block3D(new Point3D(x, b - h, 0f), new Size3D(w, h, d));
        }
        #endregion
    }
    /// <summary>
    /// GraphDataItemPieInfo : podpůrná data pro jednu každou položku grafu Bar
    /// </summary>
    public class GraphDataItemPieInfo
    {
        /// <summary>
        /// Střed elipsy.
        /// </summary>
        public PointF ArcCenter { get; set; }
        /// <summary>
        /// 2D prostor elipsy plný, bez aplikace náklonu.
        /// </summary>
        public RectangleF PieArea { get; set; }
        /// <summary>
        /// Úhel náklonu v radiánech.
        /// 0 = kolmo k pozorovateli (kruh).
        /// +Pi/2 (= +90°) = leží čelem nahoru.
        /// -Pi/2 (= -90°) = leží čelem dolů.
        /// </summary>
        public double TiltAngle { get; set; }
        /// <summary>
        /// Poloměr kruhu = 1/2 šířky elipsy (šířka se náklonem nemění).
        /// </summary>
        public float Radius { get; set; }
        /// <summary>
        /// Úhel ve stupních (360°), kde Pie začíná.
        /// Hodnota 0° = vpravo na ose X.
        /// Pie začíná nahoře, na úhlu -90°.
        /// </summary>
        public float StartAngle { get; set; }
        /// <summary>
        /// Délka oblouku ve stupních (360°), odpovídá hodnotě prvku (Value).
        /// </summary>
        public float SweepAngle { get; set; }
        /// <summary>
        /// Vzdálenost nejbližšího bodu od pozorovatele, kvůli pořadí vykreslování 3D objektů (kreslit se začíná od nejvzdálenějších objektů).
        /// </summary>
        public float ObserverDistance { get; set; }
        public static int CompareByObserverDistanceDesc(GraphDataItem<GraphDataItemPieInfo> a, GraphDataItem<GraphDataItemPieInfo> b)
        {
            return b.GraphItemInfo.ObserverDistance.CompareTo(a.GraphItemInfo.ObserverDistance);
        }
    }
    #endregion
}
